package qy.jalgotrade.strategy.position;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import qy.jalgotrade.broker.Order;
import qy.jalgotrade.broker.OrderEvent;
import qy.jalgotrade.broker.OrderExecutionInfo;
import qy.jalgotrade.stratanalyzer.returns.PositionTracker;
import qy.jalgotrade.strategy.BaseStrategy;

/**
 * <pre>
 * Base class for positions.
 * 
 * Positions are higher level abstractions for placing orders. They are escentially a pair of
 * entry-exit orders and allow to track returns and PnL easier that placing orders manually.
 * 
 * .. note::
 *     This is a base class and should not be used directly.
 * </pre>
 * 
 * @author qy
 *
 */
public abstract class Position {

	private PositionState __state;

	private Map<String, Order> __activeOrders;

	private double __shares;

	private BaseStrategy __strategy;

	private Order __entryOrder;

	private ZonedDateTime __entryDateTime;

	private Order __exitOrder;

	private ZonedDateTime __exitDateTime;

	private PositionTracker __posTracker;

	private boolean __allOrNone;

	/**
	 * 
	 * @param strategy         The strategy that this position belongs to.
	 * @param entryOrder       The order used to enter the position.
	 * @param goodTillCanceled True if the entry order should be set as good till canceled.
	 * @param allOrNone        True if the orders should be completely filled or not at all.
	 * @throws Exception
	 */
	public Position(BaseStrategy strategy, Order entryOrder, boolean goodTillCanceled, boolean allOrNone)
	        throws Exception {

		// The order must be created but not submitted.
		assert entryOrder.isInitial();

		__state = null;
		__activeOrders = new HashMap<>();
		__shares = 0;
		__strategy = strategy;
		__entryOrder = null;
		__entryDateTime = null;
		__exitOrder = null;
		__exitDateTime = null;
		__posTracker = new PositionTracker(entryOrder.getInstrumentTraits());
		__allOrNone = allOrNone;

		switchState(new WaitingEntryState());

		entryOrder.setGoodTillCanceled(goodTillCanceled);
		entryOrder.setAllOrNone(allOrNone);
		__submitAndRegisterOrder(entryOrder);

		__entryOrder = entryOrder;
	}

	/**
	 * XXX: Java requires calling super() at the entry of sub class constructor. Only used by sub classes.
	 */
	protected Position() {

	}

	/**
	 * XXX: Java requires calling super() at the entry of sub class constructor. Only used by sub classes.
	 * 
	 * @param strategy
	 * @param entryOrder
	 * @param goodTillCanceled
	 * @param allOrNone
	 * @throws Exception
	 */
	protected void _init(BaseStrategy strategy, Order entryOrder, boolean goodTillCanceled, boolean allOrNone)
	        throws Exception {

		// The order must be created but not submitted.
		assert entryOrder.isInitial();

		__state = null;
		__activeOrders = new HashMap<>();
		__shares = 0;
		__strategy = strategy;
		__entryOrder = null;
		__entryDateTime = null;
		__exitOrder = null;
		__exitDateTime = null;
		__posTracker = new PositionTracker(entryOrder.getInstrumentTraits());
		__allOrNone = allOrNone;

		switchState(new WaitingEntryState());

		entryOrder.setGoodTillCanceled(goodTillCanceled);
		entryOrder.setAllOrNone(allOrNone);
		__submitAndRegisterOrder(entryOrder);

		__entryOrder = entryOrder;
	}

	private void __submitAndRegisterOrder(Order order) throws Exception {

		assert order.isInitial();

		// Check if an order can be submitted in the current state.
		__state.canSubmitOrder(this, order);

		/*
		 * This may raise an exception, so we wan't to submit the order before moving forward and registering
		 * the order in the strategy.
		 */
		getStrategy().getBroker().submitOrder(order);

		__activeOrders.put(order.getId(), order);
		getStrategy().registerPositionOrder(this, order);
	}

	public void setEntryDateTime(ZonedDateTime dateTime) {

		__entryDateTime = dateTime;
	}

	public void setExitDateTime(ZonedDateTime dateTime) {

		__exitDateTime = dateTime;
	}

	public void switchState(PositionState newState) {

		__state = newState;
		__state.onEnter(this);
	}

	public BaseStrategy getStrategy() {

		return __strategy;
	}

	/**
	 * Returns the :class:`pyalgotrade.broker.Order` used to enter the position.
	 * 
	 * @return
	 */
	public Order getEntryOrder() {

		return __entryOrder;
	}

	/**
	 * Returns the :class:`pyalgotrade.broker.Order` used to exit the position. If this position hasn't
	 * been closed yet, None is returned.
	 * 
	 * @return
	 */
	public Order getExitOrder() {

		return __exitOrder;
	}

	/**
	 * Returns the instrument used for this position.
	 * 
	 * @return
	 */
	public String getInstrument() {

		return __entryOrder.getInstrument();
	}

	public double getLastPrice() {

		return __strategy.getLastPrice(getInstrument());
	}

	/**
	 * Calculates cumulative percentage returns up to this point. If the position is not closed, these
	 * will be unrealized returns.
	 * 
	 * @return
	 */
	public double getReturn() {

		double ret = 0.0;
		double price = getLastPrice();
		if (!Double.isNaN(price)) {
			ret = __posTracker.getReturn(price);
		}
		return ret;
	}

	/**
	 * Calculates PnL up to this point. If the position is not closed, these will be unrealized PnL.
	 * 
	 * @return
	 */
	public double getPnL() {

		double ret = 0.0;
		double price = getLastPrice();
		if (!Double.isNaN(price)) {
			ret = __posTracker.getPnL(price);
		}
		return ret;
	}

	public List<Order> getActiveOrders() {

		return new ArrayList<>(__activeOrders.values());
	}

	/**
	 * <pre>
	 * Returns the number of shares.
	 * This will be a possitive number for a long position, and a negative number for a short position.
	 * 
	 * .. note::
	 *     If the entry order was not filled, or if the position is closed, then the number of
	 * shares will be 0.
	 * </pre>
	 * 
	 * @return
	 */
	public double getShares() {

		return __shares;
	}

	/**
	 * Returns True if the position is open.
	 * 
	 * @return
	 */
	public boolean isOpen() {

		return __state.isOpen(this);
	}

	/**
	 * <pre>
	 * Returns the duration in open state.
	 * 
	 * .. note::
	 *     * If the position is open, then the difference between the entry datetime and the datetime of the last bar is returned.
	 *     * If the position is closed, then the difference between the entry datetime and the exit datetime is returned.
	 * </pre>
	 * 
	 * @return datetime.timedelta.
	 */
	public Duration getAge() {

		Duration ret = Duration.ZERO;
		ZonedDateTime last;
		if (__entryDateTime != null) {
			if (__exitDateTime != null) {
				last = __exitDateTime;
			} else {
				last = __strategy.getCurrentDateTime();
			}
			ret = Duration.between(__entryDateTime, last);
		}
		return ret;
	}

	/**
	 * Returns True if the entry order is active.
	 * 
	 * @return
	 */
	public boolean entryActive() {

		return __entryOrder != null && __entryOrder.isActive();
	}

	/**
	 * Returns True if the entry order was filled.
	 * 
	 * @return
	 */
	public boolean entryFilled() {

		return __entryOrder != null && __entryOrder.isFilled();
	}

	/**
	 * Returns True if the exit order is active.
	 * 
	 * @return
	 */
	public boolean exitActive() {

		return __exitOrder != null && __exitOrder.isActive();
	}

	/**
	 * Returns True if the exit order was filled.
	 * 
	 * @return
	 */
	public boolean exitFilled() {

		return __exitOrder != null && __exitOrder.isFilled();
	}

	/**
	 * Cancels the entry order if its active.
	 * @throws Exception 
	 */
	public void cancelEntry() throws Exception {

		if (entryActive()) {
			getStrategy().getBroker().cancelOrder(getEntryOrder());
		}
	}

	/**
	 * Cancels the exit order if its active.
	 * @throws Exception 
	 */
	public void cancelExit() throws Exception {

		if (exitActive()) {
			getStrategy().getBroker().cancelOrder(getExitOrder());
		}
	}

	/**
	 * <pre>
	 * Submits a market order to close this position.
	 * 
	 * .. note::
	 *     * If the position is closed (entry canceled or exit filled) this won't have any effect.
	 *     * If the exit order for this position is pending, an exception will be raised. The exit order should be canceled first.
	 *     * If the entry order is active, cancellation will be requested.
	 * </pre>
	 * 
	 * @throws Exception
	 */
	public void exitMarket() throws Exception {

		exitMarket(false);
	}

	/**
	 * <pre>
	 * Submits a market order to close this position.
	 * 
	 * .. note::
	 *     * If the position is closed (entry canceled or exit filled) this won't have any effect.
	 *     * If the exit order for this position is pending, an exception will be raised. The exit order should be canceled first.
	 *     * If the entry order is active, cancellation will be requested.
	 * </pre>
	 * 
	 * @param goodTillCanceled True if the exit order is good till canceled. If False then the order
	 *                         gets automatically canceled when the session closes. If None, then it
	 *                         will match the entry order.
	 * @throws Exception 
	 */
	public void exitMarket(boolean goodTillCanceled) throws Exception {

		__state.exit(this, Double.NaN, Double.NaN, goodTillCanceled);
	}

	/**
	 * <pre>
	 * Submits a limit order to close this position.
	 * 
	 * .. note::
	 *     * If the position is closed (entry canceled or exit filled) this won't have any effect.
	 *     * If the exit order for this position is pending, an exception will be raised. The exit order should be canceled first.
	 *     * If the entry order is active, cancellation will be requested.
	 * </pre>
	 * 
	 * @param limitPrice The limit price.
	 * @throws Exception
	 */
	public void exitLimit(double limitPrice) throws Exception {

		exitLimit(limitPrice, false);
	}

	/**
	 * <pre>
	 * Submits a limit order to close this position.
	 * 
	 * .. note::
	 *     * If the position is closed (entry canceled or exit filled) this won't have any effect.
	 *     * If the exit order for this position is pending, an exception will be raised. The exit order should be canceled first.
	 *     * If the entry order is active, cancellation will be requested.
	 * </pre>
	 * 
	 * @param limitPrice       The limit price.
	 * @param goodTillCanceled True if the exit order is good till canceled. If False then the order
	 *                         gets automatically canceled when the session closes. If None, then it
	 *                         will match the entry order.
	 * @throws Exception 
	 */
	public void exitLimit(double limitPrice, boolean goodTillCanceled) throws Exception {

		__state.exit(this, Double.NaN, limitPrice, goodTillCanceled);
	}

	/**
	 * <pre>
	 * Submits a stop order to close this position.
	 * 
	 * .. note::
	 *     * If the position is closed (entry canceled or exit filled) this won't have any effect.
	 *     * If the exit order for this position is pending, an exception will be raised. The exit order should be canceled first.
	 *     * If the entry order is active, cancellation will be requested.
	 * </pre>
	 * 
	 * @param stopPrice The stop price.
	 * @throws Exception
	 */
	public void exitStop(double stopPrice) throws Exception {

		exitStop(stopPrice, false);
	}

	/**
	 * <pre>
	 * Submits a stop order to close this position.
	 * 
	 * .. note::
	 *     * If the position is closed (entry canceled or exit filled) this won't have any effect.
	 *     * If the exit order for this position is pending, an exception will be raised. The exit order should be canceled first.
	 *     * If the entry order is active, cancellation will be requested.
	 * </pre>
	 * 
	 * @param stopPrice        The stop price.
	 * @param goodTillCanceled True if the exit order is good till canceled. If False then the order
	 *                         gets automatically canceled when the session closes. If None, then it
	 *                         will match the entry order.
	 * @throws Exception 
	 */
	public void exitStop(double stopPrice, boolean goodTillCanceled) throws Exception {

		__state.exit(this, stopPrice, Double.NaN, goodTillCanceled);
	}

	/**
	 * <pre>
	 * Submits a stop limit order to close this position.
	 * 
	 * .. note::
	 *     * If the position is closed (entry canceled or exit filled) this won't have any effect.
	 *     * If the exit order for this position is pending, an exception will be raised. The exit order should be canceled first.
	 *     * If the entry order is active, cancellation will be requested.
	 * </pre>
	 * 
	 * @param stopPrice The stop price.
	 * @param limitPrice The limit price.
	 * @throws Exception
	 */
	public void exitStopLimit(double stopPrice, double limitPrice) throws Exception {

		exitStopLimit(stopPrice, limitPrice, false);
	}

	/**
	 * <pre>
	 * Submits a stop limit order to close this position.
	 * 
	 * .. note::
	 *     * If the position is closed (entry canceled or exit filled) this won't have any effect.
	 *     * If the exit order for this position is pending, an exception will be raised. The exit order should be canceled first.
	 *     * If the entry order is active, cancellation will be requested.
	 * </pre>
	 * 
	 * @param stopPrice        The stop price.
	 * @param limitPrice       The limit price.
	 * @param goodTillCanceled True if the exit order is good till canceled. If False then the order
	 *                         gets automatically canceled when the session closes. If None, then it
	 *                         will match the entry order.
	 * @throws Exception 
	 */
	public void exitStopLimit(double stopPrice, double limitPrice, boolean goodTillCanceled) throws Exception {

		__state.exit(this, stopPrice, limitPrice, goodTillCanceled);
	}

	/**
	 * 
	 * @param stopPrice
	 * @param limitPrice
	 * @return
	 * @throws Exception 
	 */
	public abstract Order buildExitOrder(double stopPrice, double limitPrice) throws Exception;

	/**
	 * 
	 * @param orderEvent
	 * @throws Exception
	 */
	public void onOrderEvent(OrderEvent orderEvent) throws Exception {

		__updatePosTracker(orderEvent);

		Order order = orderEvent.getOrder();
		if (!order.isActive()) {
			__activeOrders.remove(order.getId());
		}

		// Update the number of shares.
		if (Arrays.asList(OrderEvent.Type.PARTIALLY_FILLED, OrderEvent.Type.FILLED)
		        .contains(orderEvent.getEventType())) {
			OrderExecutionInfo execInfo = orderEvent.getEventInfo();
			// roundQuantity is used to prevent bugs like the one triggered in testcases.bitstamp_test:TestCase.testRoundingBug
			if (order.isBuy()) {
				__shares = order.getInstrumentTraits().roundQuantity(__shares + execInfo.getQuantity());
			} else {
				__shares = order.getInstrumentTraits().roundQuantity(__shares - execInfo.getQuantity());
			}
		}

		__state.onOrderEvent(this, orderEvent);
	}

	/**
	 * 
	 * @param stopPrice
	 * @param limitPrice
	 * @param goodTillCanceled
	 * @throws Exception
	 */
	protected void _submitExitOrder(double stopPrice, double limitPrice, boolean goodTillCanceled) throws Exception {

		assert !exitActive();

		Order exitOrder = buildExitOrder(stopPrice, limitPrice);
		// If goodTillCanceled was not set, match the entry order.
		exitOrder.setGoodTillCanceled(goodTillCanceled);
		exitOrder.setAllOrNone(__allOrNone);

		__submitAndRegisterOrder(exitOrder);
		__exitOrder = exitOrder;
	}

	private void __updatePosTracker(OrderEvent orderEvent) {

		if (Arrays.asList(OrderEvent.Type.PARTIALLY_FILLED, OrderEvent.Type.FILLED)
		        .contains(orderEvent.getEventType())) {

			Order order = orderEvent.getOrder();
			OrderExecutionInfo execInfo = orderEvent.getEventInfo();
			if (order.isBuy()) {
				__posTracker.buy(execInfo.getQuantity(), execInfo.getPrice(), execInfo.getCommission());
			} else {
				__posTracker.sell(execInfo.getQuantity(), execInfo.getPrice(), execInfo.getCommission());
			}
		}
	}
}
